Introduction
While customizing my website’s style, I thought about having light and dark themes for both my posts and the plots included in them, eventually stumbling by Mickaël Canouil’s post about having this option for ggplot2 plots.
However, I also had plotly plots and gt tables which required different treatment in the script as plotly produced SVG files and gt tables were simply tables in HTML but much more complex due to their special structure.
Code
As mentioned in Mickaël Canouil’s post, we have to use knitr
as the engine to generate the images; the only difference is that I created an alternative knitr
handler for the light theme identical to the dark one.
Steps to follow :
- Include this code in the YAML header of your Quarto document. (Don’t forget to enable light and dark themes for your page)
knitr:
opts_chunk:
dev: [lightsvglite, darksvglite]
fig.ext: [.light.svg, .dark.svg]
theme:
- light: flatly
- dark: darkly
- Add your ggplot2 themes and customize it to your likening.
light_theme <- function() {
ggthemes::theme_solarized_2() %+%
theme(
plot.background = element_rect(fill = "#FFF1E5"),
panel.border = element_blank(),
axis.line = element_line(colour = "#586e75",
linetype = 1),
axis.ticks = element_line(colour = "#586e75"),
axis.text = element_text(colour = "#002b36"),
legend.background = element_rect(fill = "#FFF1E5"))
}
theme_set(light_theme())
lightsvglite <- function(file, width, height) {
on.exit(reset_theme_settings())
theme_set(light_theme())
ggsave(
filename = file,
width = width,
height = height,
dev = "svg",
bg = "transparent"
)
}
theme_dark <- function() {
ggthemes::theme_solarized_2(light = F) %+%
theme(
text = element_text(colour = "white"),
axis.text = element_text(colour = "white"),
axis.title = element_text(colour = "white"),
legend.text = element_text(colour = "white"),
legend.title = element_text(colour = "white"),
strip.text = element_text(colour = "white"),
rect = element_rect(colour = "#272b30", fill = "#272b30"),
plot.background = element_rect(fill = "#222222", colour = NA),
axis.line = element_line(colour = "white"),
axis.ticks = element_line(colour = "white"),
plot.title = element_text(colour = "white"),
plot.subtitle = element_text(colour = "white"),
plot.caption = element_text(colour = "white"),
legend.background = element_rect(fill = "#222222")
)
}
darksvglite <- function(file, width, height) {
on.exit(reset_theme_settings())
theme_set(theme_dark())
ggsave(
filename = file,
width = width,
height = height,
dev = "svg",
bg = "transparent"
)
}
You can store the ggplot2 themes R scripts in a folder and call it in each post by using the source() function or even creating a personal package containing what you need.
- Finally add this to your YAML header to include the Javascript code in your Quarto document and you’re good to go!
include-after-body:
text: |
<script type="application/javascript" src="light-dark.js"></script>
Javascript code
// Author: Aymen Nasri
// Version: <1.1.0>
// Description: Change plots theme depending on body class (quarto-light or quarto-dark)
// Originally made by Mickaël Canouil
// License: MIT
function updateImageSrc() {
// Identifying which theme is on
const isLightMode = document.body.classList.contains('quarto-light');
const isDarkMode = document.body.classList.contains('quarto-dark');
if (!isLightMode && !isDarkMode) return; // Exit if neither mode is active
// Function to update styles
const updateElements = (selector, updateFunc) => {
document.querySelectorAll(selector).forEach(updateFunc);
;
}
// Function to replace the plots depending on theme
updateElements('img', img => {
const newSrc = img.src.replace(isLightMode ? '.dark' : '.light', isDarkMode ? '.dark' : '.light');
if (newSrc !== img.src) img.src = newSrc;
;
})
// Update ggplot
const updateStyle = (elem, prop, lightValue, darkValue) => {
const currentValue = elem.style[prop];
const newValue = isDarkMode ? darkValue : lightValue;
if (currentValue !== newValue) elem.style[prop] = newValue;
;
}
// Update ploly background color for both the plot and the legend box
updateElements('svg[style*="background"]', svg => updateStyle(svg, 'background', 'rgb(255, 241, 229)', 'rgb(34, 34, 34)'));
updateElements('rect[style*="fill"]', rect => updateStyle(rect, 'fill', 'rgb(255, 241, 229)', 'rgb(34, 34, 34)'));
// Save the original plotly styling
updateElements('text[class*="legendtext"], svg text, svg tspan', text => {
if (!text.dataset.originalStyle) {
const computedStyle = window.getComputedStyle(text);
.dataset.originalStyle = JSON.stringify({
textfill: computedStyle.fill,
color: computedStyle.color,
fontSize: computedStyle.fontSize,
fontWeight: computedStyle.fontWeight,
fontFamily: computedStyle.fontFamily,
textDecoration: computedStyle.textDecoration
;
})
}
const originalStyle = JSON.parse(text.dataset.originalStyle);
// Modify the text colors for plotly labels
if (isDarkMode) {
.style.fill = 'white';
text.style.color = 'white';
textelse {
} Object.assign(text.style, originalStyle);
};
})
// Update table text color
updateElements('.gt_table_body, .gt_heading, .gt_sourcenotes, .gt_footnotes', table => {
if (isDarkMode) {
.style.color = 'white'; // Set text color to a light shade
tableelse {
} .style.color = ''; // Reset to default
table
};
})
}
// Observer making sure all changes are done
const observer = new MutationObserver(mutations => {
if (mutations.some(mutation =>
.type === 'attributes' && mutation.attributeName === 'class') ||
(mutation.type === 'childList' && mutation.target.tagName === 'svg'))) {
(mutationupdateImageSrc();
};
})
.observe(document.body, {
observerattributes: true,
childList: true,
subtree: true
;
})
// Run on page load and immediately
document.addEventListener('DOMContentLoaded', updateImageSrc);
updateImageSrc();
What if you have other figures not included in the script?
A useful tool for my work was the Web Developer Tools on Firefox where I inspected different parts of the rendered HTML file to later include them in the script by name, you could use that in order to add more support for different figures.
You can visit my Github repo (link under the table of contents) and contact me there if you need something.